Conversation
❌ ErrorsYour PR has failed checks. Please review the issues below and take necessary action before merging. 🚦 17 Pipeline jobs failed
Useful? React with 👍 / 👎 This comment will be updated automatically if new data arrives.🔗 Commit SHA: a1a4480 | Docs | View more details | Give us feedback! |
CI Test ResultsRun: #36152227381 | Commit:
Status Overview
Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled Failed Jobs
Summary: Total: 32 | Passed: 18 | Failed: 14 Updated: 2026-09-25 15:27:41 UTC |
a78aa75 to
36ff97c
Compare
36ff97c to
09d5f7d
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 63de2f9c71
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (nm->isFrameCompleteAt(walk_pc.raw())) { | ||
| const void* epilogue_pc = walk_pc.raw(); | ||
| if (depth == 1 && frame.unwindEpilogue(nm, (uintptr_t&)epilogue_pc, sp, fp)) { | ||
| walk_pc.setExactAddress(epilogue_pc); |
There was a problem hiding this comment.
Preserve return-address state after arm64 JIT unwinding
On supported arm64, unwindEpilogue, unwindPrologue, and unwindStub assign pc from StackFrame::link(), which is the caller's raw return address; this records it as exact, so the following native resolution and DWARF lookup do not subtract one byte. A sample that leaves a generated frame into a native caller whose call site ends at a symbol/FDE boundary is therefore still attributed to the following symbol/row (the same classification is repeated at lines 676 and 732). Mark these outputs as return addresses, or have the unwind helpers report whether they already adjusted the PC.
AGENTS.md reference: AGENTS.md:L199-L203
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
The three arch unwind helpers were all treated as having already applied the attribution adjustment. That holds on x86_64, which subtracts one inside the helper, but not on aarch64: every branch walkVM can reach there assigns the link register or a saved-pc slot, both raw return addresses. The one aarch64 branch that does subtract is guarded by `&pc == &this->pc()`, true only on the AsyncGetCallTrace path where the caller passes the frame's own pc rather than a local, so walkVM never takes it. A sample leaving a generated frame for a native caller whose call is the last instruction before a symbol or FDE boundary was therefore still attributed to whatever follows, on arm64, for exactly the frames this change set out to fix. recordUnwoundPc() names the difference in one place. x86_64 behaviour is unchanged; the distinction disappears once the helpers agree on a contract. Reported by Codex review on #813. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
33f8634 to
77538df
Compare
walkVM fed the raw walking pc to findLibraryByAddress, findFrameDesc and the DW_REG_PLT stub-offset test. Past the leaf that pc is a return address, so a call that is the last instruction of its caller selected the following function's CFA row, derived a sender sp from it, and could miss a MARK_THREAD_ENTRY sitting on the caller -- the same defect already fixed in StackWalker::walkFP/walkDwarf, on the path CSTACK_DEFAULT actually resolves to. Track per-frame whether the pc came out of a return-address slot and route the range-based lookups through attributionPC(). Exact-address consumers (isContReturnBarrier, isContEntryReturnPc, isEntryFrame), the DW_PC_OFFSET arithmetic and the no-progress guard keep the raw pc, and a signal-frame CIE suppresses the adjustment exactly as it does in walkDwarf. resolveNativeFrameForWalkVM was using one address for two jobs. It now takes the attribution address for findLibraryByAddress/binarySearch while the emitted pc_offset keeps deriving from the raw pc, so the remote-symbolication wire value is unchanged and no cross-team contract moves. unwindPrologue/unwindEpilogue/unwindStub are left alone: x86_64 already folds the adjustment into the pc they return and does so inconsistently (the isFrameComplete branch omits it) while aarch64 returns it raw, so their results are flagged as non-return-addresses and cannot be adjusted twice. Unifying that contract also fixes the exact-address comparisons those helpers currently break on x86_64, and is left to its own change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…resolution resolveNativeFrameForWalkVM uses two addresses for two jobs: range lookups key off the attribution address so a call that is the last instruction of its caller still resolves to the caller, while the emitted pc_offset keeps deriving from the raw pc because its meaning is a wire contract rather than a local detail. Neither half was gated by a test. Both are now pinned at a synthetic zero-gap symbol boundary, the shape where the two addresses disagree: flagging the pc as a return address must move the resolved symbol from the following function back to the caller, and must leave the emitted offset untouched. A third case pins that an address well inside a function resolves identically either way, so the adjustment cannot be read as a blanket shift. Both assertions were mutation-checked -- reverting the lookup to the raw pc fails the boundary case alone, and deriving pc_offset from the attribution address fails the wire case alone. The fixtures publish synthetic CodeCaches through a new test-only entry point rather than the test binary's own symbols, which is what makes a controlled zero-gap boundary possible at all and keeps these tests free of the GNU-as/ELF-CFI and updateSymbols() dependencies that confine returnAddressAttribution_ut to Linux. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
walkVM tracked "is this pc a return address?" in a bool sitting beside the pc, so the two could drift: a newly added `pc = ...` inherited whatever the previous frame had set, silently attributing the next lookup to the wrong address. That is the exact hazard WalkPc was introduced for in walkFP/walkDwarf, and walkVM is the default cstack path, so it is the one that most needed it. walkVM now drives a WalkPc. Every pc source states what it produced -- setReturnAddress for a saved-pc slot or link register, setRecoveredPc for a DWARF row, setExactAddress where the arch unwind helpers already folded the adjustment in -- and consumers read raw() or attribution() explicitly. Adding a pc source is now a compile-time decision instead of something a reviewer has to notice. Threading the seed through surfaced a real gap: walkVM is entered either from a ucontext, whose pc is the exact interrupted address, or from callerPC(), which is a genuine return address on every architecture where CALLER_PC_IS_RETURN_ADDRESS holds. Both arrived as a bare pointer and were treated as exact, so the callerPC() entry never got the adjustment it needed. The private overload now takes that distinction from its caller, the same way walkFP and walkDwarf seed themselves. No behaviour change otherwise: the mutator chosen at each site reproduces the flag that site already set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the call walkVM's public entry point built its two starting states inline, so the pc and the flag describing it were separate arguments chosen a few tokens apart in two near-identical call expressions. Nothing bound them together, which is the same drift the walk itself now avoids by carrying a WalkPc. Both states are one named value instead. walkVMSeed() decides where a walk starts -- register set and the nature of the pc together -- and the entry point delegates without a branch of its own. The frame no longer outlives the decision: its pc/sp/fp are copied out as scalars, which is all the callee ever used, and the ucontext they point into outlives the walk regardless. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three review follow-ups. addLibraryForTest shipped in every build. Every other test hook in this codebase is either behind #ifdef UNIT_TEST or reached through a friend *TestAccessor, so a publicly callable mutator for the process-wide library set was both inconsistent and present where nothing should be able to call it. It is now compiled only into gtest binaries, which is where its one caller lives. prev_native_pc lost its last reader when the walk started carrying the previous frame as a WalkPc; the declaration stayed behind. Unused-variable warnings are not errors here, so nothing caught it. RemoteSymbolication.md still described the two-argument resolveNativeFrameForWalkVM. It now names the third parameter and says what it selects -- lookups move to the attribution address, the emitted pc_offset does not -- since the wire value being unchanged is the part a reader of that document needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three arch unwind helpers were all treated as having already applied the attribution adjustment. That holds on x86_64, which subtracts one inside the helper, but not on aarch64: every branch walkVM can reach there assigns the link register or a saved-pc slot, both raw return addresses. The one aarch64 branch that does subtract is guarded by `&pc == &this->pc()`, true only on the AsyncGetCallTrace path where the caller passes the frame's own pc rather than a local, so walkVM never takes it. A sample leaving a generated frame for a native caller whose call is the last instruction before a symbol or FDE boundary was therefore still attributed to whatever follows, on arm64, for exactly the frames this change set out to fix. recordUnwoundPc() names the difference in one place. x86_64 behaviour is unchanged; the distinction disappears once the helpers agree on a contract. Reported by Codex review on #813. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
77538df to
a924a47
Compare
The two copies of this unwind step have drifted. Each gap is taken from
walkDwarf, which is the one that got the attention:
- The link register was fed to findFrameDesc unstripped. On aarch64 it
carries PAC bits, so the lookup address is nonsense. walkDwarf strips it
and says why; this side never picked that up. Only reachable through a
frame table carrying DW_LINK_REGISTER, which DwarfParser does not emit
and SFrameParser does, so it is latent rather than live today.
- The frame-pointer slot was dereferenced without checking its alignment,
while the pc slot three lines below is checked. Same load, same exposure.
- Neither slot load carried a fault-injection hook, so the recovery path
around them went unexercised. LIKELY matches the existing convention:
walkVM's seven UNLIKELY sites are raw dereferences, these two go through
SafeAccess.
Reconciling these first keeps the extraction that follows a pure move, with
no behaviour hidden inside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
walkVM read its VMThread through JVMThread::current(), which asserts its thread-local key is live -- something only a JVM attach sets up. Nothing below that line needs the thread on the native path, and every use of it is already NULL-guarded, so it now asks whether one exists instead of requiring it. With a JVM attached the answer is always yes and nothing changes; without one the walk takes the same degraded path it would on an unattached thread, and WALKVM_NO_VMTHREAD still records that it did. That is the whole of what stood between walkVM and a unit test. VMStructs stays uninitialised, so CodeHeap::contains() is false for every address and the walk goes down the native branch on every frame -- which is the branch the attribution work touches. The test plants one synthetic linked frame so the walk steps off an exact ucontext leaf onto a caller whose pc comes out of a return-address slot. Both land on the same zero-gap boundary, so they must resolve to different functions purely because of that difference. Mutating the DWARF recovery to claim its pc is exact fails it, reporting the following function where the caller belongs. Two platform details the fixture has to respect, both found the hard way: uc_mcontext is a pointer on Darwin and embedded on Linux, so a zeroed ucontext_t needs storage to point at or the first register access faults in a loop behind the profiler's own SIGSEGV handler; and the frame layout has to come from FrameDesc::default_frame, which is what a table-less CodeCache returns, not fallback_default_frame(), which differs from it on Apple aarch64. This covers the DWARF recovery path only. The saved-pc slots in the compiled and stub arms sit behind CodeHeap::contains() and stay unreachable here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Handover — PROF-16059 progress, resuming on LinuxRecording state here rather than as a file in the tree, per the "no specs in repo" convention: this is a snapshot, not something anyone should have to maintain. Where this standsBranch
Everything below Step 1 — done
Behaviour with a JVM attached is unchanged by construction — Step 2 — done, and mutation-checked
Mutating the DWARF recovery to Scope, precisely: this covers the DWARF recovery path only. I checked: mutating the two Two platform traps, both cost real time
A table-less Next: Step 3 — extract
|
…step 61959b0 reconciled three gaps between walkVM's copy of the DWARF unwind step and walkDwarf's, all of them in the pc/fp recovery half. A mechanical diff of the two bodies shows the CFA half still diverges in two places, and unlike the earlier three these resolve in opposite directions -- so the extraction that follows could not have been the pure move that commit claimed it was setting up. walkVM falls through on a CFA register it does not implement. walkDwarf ends its cfa_reg chain with an else that stops the walk. walkVM instead pre-screened DW_REG_INVALID and anything past DW_REG_PLT, then ran the same three arms with no else. With DW_REG_FP 6, DW_REG_SP 7 and DW_REG_PLT 128 on x86_64, a register in 0..5 or 8..127 matches no arm and is not rejected: sp keeps the value it had on entry to the step, and the walk reads the caller's pc and fp out of the current frame's own slots. It is reachable from ordinary DWARF. DwarfParser takes the register straight off the wire under DW_CFA_def_cfa, _def_cfa_register and _def_cfa_sf, and addRecord() only rejects it above 0xFF, so nothing narrows it to the three the walkers implement -- a function whose CFA is based on any other register lands in the hole. WalkVmAlsoStopsOnAnUnhandledCfaRegister plants exactly that row and shows walkVM recording a second frame off an unmoved sp where walkDwarf stops at the leaf. walkVM's pre-screen is subsumed by the new else and goes away with it; every value it rejected now falls out of the same arm as the rest. walkDwarf trusts a frame pointer walkVM checks. The other direction: walkVM sanity-checks fp for bounds and alignment before deriving a CFA from it, walkDwarf did not. A misaligned fp whose row offset re-aligns the result slips past every later test, all of which look at sp, so only a check on fp itself stops it. 61959b0's rule was that each gap is taken from walkDwarf; this one goes the other way, and the comment travels with the code so the reason is not lost. Both changes are gated, each verified to fail with its fix reverted: WalkDwarfStopsOnAnUnhandledCfaRegister baseline, passes throughout WalkVmAlsoStopsOnAnUnhandledCfaRegister gates the else WalkDwarfRejectsAMisalignedFramePointer gates the fp check Two properties of the fixture are load-bearing and commented as such: DW_PC_OFFSET is 1, so fp_off must be even or the step takes the DW_OP_breg arithmetic branch instead of the memory-slot one under test; and cfa_off must be non-zero or the aarch64 defaultSenderSP() branch fires. A DW_REG_INVALID row above each planted leaf stops whichever walker does step past it, so a failure reports a wrong depth rather than running into unplanted memory. With this the two bodies differ only in where attribution_pc is declared and in walkVM's prev_native_walk_pc/have_prev_native_pc bookkeeping, which stays at the call site. advanceDwarfFrame() can now be extracted as a pure move. Full native suite green on linux-x64 in debug and release, and this binary green in all four configurations. That is also the first Linux run of the walkVM fixtures, which had only been exercised on macOS/arm64. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rame walkDwarf and walkVM carried two copies of the same unwind step. 61959b0 and 06ab5b6 reconciled the five differences between them; this moves what is left into one always_inline helper in dwarfStep.inline.h and calls it from both. 183 lines of duplication become 10. always_inline is load-bearing, not an optimisation. AddressSanitizer instruments per function after inlining, so an inlined body takes its caller's sanitizer attribute. walkVM is no_sanitize("address") because it reads arbitrary stack memory while sampling; walkDwarf is deliberately instrumented, and both have been that way since before the two were split apart in #451. Out of line, one attribute would have to serve both -- either dropping walkDwarf's instrumentation or adding to walkVM the instrumentation the attribute exists to avoid. Measured on the built objects rather than assumed: with always_inline the helper emits no out-of-line symbol at all, walkVM still contains zero __asan references and walkDwarf still contains its own. What moved and what did not: - Both callers keep their own `bottom`, computed from their own frame, and pass it in. - `depth` is a parameter because the two callers disagree on it. walkVM's fp-chain fallback reaches the step through `goto dwarf_unwind`, which skips its fillFrame(frames[depth++], ...), so on that path it arrives one lower than walkDwarf does at the same logical position. Passing it through is what keeps this a move rather than a merge; the header says so at the parameter. - The lookup goes inside, so the helper takes Profiler* rather than the CodeCache* the ticket sketched: the DW_REG_PLT arm needs attribution_pc to decide the stub offset, so a caller-side lookup would have to recompute walk_pc.attribution() anyway. - walkVM's prev_native_walk_pc/have_prev_native_pc bookkeeping stays at the call site, hoisted to just above the call from the middle of the step. Nothing between those two points mutates walk_pc, and a stop leaves the loop, so the only reader -- the MARK_THREAD_ENTRY check on the next iteration -- cannot tell the difference. Each of the nine break sites becomes a `return false`; the count was checked mechanically before and after rather than by eye, since four of them sit on arms no test in the suite reaches. Equivalence was checked against the disassembly, not only the tests. walkDwarf's call sequence is identical instruction-for-instruction in release; walkVM's call multiset is identical with one static-guard pair reordered. Neither is byte-identical -- inlining a function changes what the scheduler and register allocator see, so release moves 313 -> 308 instructions in walkDwarf and 2997 -> 3061 in walkVM. The sanitizer configs compile at -O0, where the helper's parameters and its FrameDesc copy materialise as real stack traffic, so walkDwarf grows there (1020 -> 1173 instructions, 9 -> 25 instrumented accesses). Those configs are test-only; the shipped release build is the one above. Full native suite green on linux-x64 in debug and release, and the three binaries over this code green under ASan and TSan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…test fails Revert before merge. Diagnostic only, no product change. gtestDebug_walkVmAttribution_ut fails on every aarch64 cell and has since 66eea0c, while amd64 is green in every configuration. Which assertion fails is not recoverable from the logs: the Gradle daemon swallows the test binary's stdout, so the job reports only the task name. .gitlab/sanitizer-tests already works around this by using Gradle for compile+link and then running each binary straight from the shell; this does the same for one binary on the glibc-aarch64 job. Placed before the Test step, which would otherwise fail the job first, and marked continue-on-error so it can only add output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses PROF-15955.
Summary
HotspotSupport::walkVMfed the raw walking pc tofindLibraryByAddress,findFrameDescand theDW_REG_PLTstub-offset test. Past the leaf that pc is a return address, so a call that is the last instruction of its caller selected the following function's CFA row, derived a sender sp from it, and could miss aMARK_THREAD_ENTRYsitting on the caller. Same defect #786 fixed inStackWalker::walkFP/walkDwarf— except this is the pathCSTACK_DEFAULTactually resolves to whenever the JVM exposes VMStructs on Linux, so it is the common production configuration rather than a fallback.walkVM now carries the pc and whether it came out of a return-address slot as a
single
WalkPcvalue — the type #786 introduced inwalkFP/walkDwarfforexactly this hazard — and routes the range-based lookups through its
attribution(). Every pc source states what it produced (setReturnAddress,setRecoveredPc,setExactAddress,setSeed), so adding one is a compile-timedecision rather than something a reviewer has to catch.
Adopting it surfaced a second defect, now also fixed here: walkVM is entered
either from a ucontext (pc is the exact interrupted address) or from
callerPC()(a genuine return address whereverCALLER_PC_IS_RETURN_ADDRESSholds, i.e. everywhere but aarch64). Both arrived as a bare pointer and were
treated as exact, so the
callerPC()entry never got the adjustment. Theprivate overload now takes that distinction from its caller, the same way
walkFP/walkDwarfseed themselves.Which pcs get adjusted, and which deliberately do not
findLibraryByAddress,findFrameDescDW_REG_PLTstub-offset testisContReturnBarrier,isContEntryReturnPc,isEntryFrameDW_PC_OFFSETarithmeticDW_OP_breg<PC>names the pc register valueinDeadZoneisFrameCompleteAt,findScopeOffset(PcDesc)A signal-frame CIE suppresses the adjustment exactly as it does in
walkDwarf(pc_is_ra = !f.isSignalFrame()).Every site in walkVM that writes
pc— 16 of them — sets the flag. Two were missing from the original enumeration in the ticket: both in-loopanchor->getFrame(pc, sp, fp)calls silently rewritepctolastJavaPC(), which is a return address.The wire format does not move
resolveNativeFrameForWalkVMwas using one address for two jobs: range lookups and the emitted remote-symbolicationpc_offset. It now takes the attribution address forfindLibraryByAddress/binarySearchwhilepc_offsetkeeps deriving from the raw pc, so the emitted wire value is byte-identical.This matters for review: it means this PR does not depend on the unresolved cross-team question about
pc_offsetsemantics raised in #786. It is effectively option (b) from that PR's own list of options, applied locally.Two findings that correct the ticket's premises
The suspected PcDesc double-subtraction bug does not exist. PROF-15955 flagged that x64's ad hoc
-1being re-fed intoisFrameCompleteAt/findScopeOffsetmight be a second latent bug. It is not:findScopeOffset(vmStructs.cpp:798) is a ceiling search — on an exact miss it returns the first PcDesc with_pc >= pc_offset— and HotSpot records PcDescs at return addresses, soRA-1andRAresolve to the same entry.isFrameCompleteAtis a monotone threshold. Both absorb the decrement, so the PcDesc sites are safe on the raw pc.There is a different, real x86_64 bug — filed as PROF-16033, not fixed here.
unwindPrologue/unwindEpilogue/unwindStubfold-1into the pc they return on x86_64 but not on aarch64, so on x86_64 the three exact-equality comparisons above always fail for pcs arriving through those helpers: continuation-boundary and entry-frame detection silently do not fire. The adjustment is also branch-inconsistent (unwindPrologue'sisFrameCompletebranch athotspotStackFrame_x64.cpp:155omits it), which is why a correct flag cannot be threaded without changing those helpers' contract.Those three call sites are therefore flagged
pc_is_ra = falsehere — conservative, cannot double-adjust, and no behaviour change on either arch. The arch files are untouched. Fixing them properly means touching x86_64/aarch64 pattern-matching code shared with the legacygetJavaTraceAsync/AsyncGetCallTrace path, which is a different blast radius and belongs in its own change.Deliberately left untouched
Per the ticket's step 5, calling these out so they do not read as oversights:
CodeHeap::findNMethod,isFrameCompleteAt,findScopeOffset→ PcDesc) stay on the raw pc — correct by HotSpot's own convention, confirmed above.unwindPrologue/unwindEpilogue/unwindStub— PROF-16033.unwindCompiled— same inconsistency, but only reachable from the AGCT path, not walkVM. Covered by PROF-16033.PerfEvents::walkKernel— carries a related defect but is outside PROF-15955's scope; it is a pure consumer of the kernel's ring buffer and would be a much smaller separate change.Test plan
buildDebug/buildRelease— cleangtestDebug— 67 test binaries, 0 failureswalkVmAttribution_ut.cppgates the address split (new, 3 tests)resolveNativeFrameForWalkVMuses two addresses for two jobs, and both halvesare now pinned at a synthetic zero-gap symbol boundary, the shape where the two
disagree:
ReturnAddressAtZeroGapBoundaryResolvesToTheCallerPcOffsetStaysDerivedFromTheRawPcpc_offsetuntouched — the attribution address is a lookup detail and must not reach the wireAddressInsideAFunctionResolvesTheSameEitherWayBoth assertions were mutation-checked against the production code:
pc_offsetderived from the attribution addressEach mutation kills exactly its intended test, so the coverage is targeted
rather than incidental.
The fixtures publish synthetic
CodeCaches via a new test-onlyLibraries::addLibraryForTest()(matching the existing*ForTestidiom inprofiler.h,os.h,callTraceHashTable.h). That is what makes a controlledzero-gap boundary possible; using the test binary's own symbols cannot produce
one. It also keeps these tests free of the GNU-as/ELF-CFI and
updateSymbols()dependencies that confinereturnAddressAttribution_uttoLinux, so they run on macOS dev builds too.
Still ungated: the 21 per-frame
WalkPcmutator calls inside walkVM. TheWalkPcconversion removes the drift hazard (a pc can no longer be assignedwithout restating its nature) but does not prove each individual choice is the
right one.
Reaching them from a test is not viable in-process, which I confirmed rather
than assumed:
walkVMcallsVMThread::current()unconditionally, which asserts_jvm_thread.isKeyValid()— a key only created byJVMThread::initialize(), whichneeds JNI into a live JVM. In a debug gtest that aborts; with the profiler's SIGSEGV
handler installed it becomes an infinite signal loop. Defeating that assert would mean
disabling the very check that says the call is invalid, so I did not.
The coupling is the real problem, and it is now tracked as PROF-16059: walkVM's
dwarf_unwindblock andwalkDwarf's loop body are two copies of one algorithm(22 of
walkDwarf's 33 code lines appear verbatim in walkVM's 56), and extractingthe shared step yields something testable with synthetic
CodeCaches — the mechanismthis PR's new tests already use. That ticket blocks PROF-16033, which has no way to
verify itself for the same reason.
Reviewers: the judgement calls worth pushing back on are (a) the
raw-vs-attribution split in the table above, (b) the new test-only entry point
on
Libraries, (c)setExactAddressas the right choice where the arch unwindhelpers already fold in their own adjustment (see PROF-16033), and (d) whether
the remaining walkVM-level gap should block merge.
🤖 Generated with Claude Code